//! Homeserver client utility, wrapping common functionality and flows for
//! clients interacting with a homeserver.

use std::io;
use std::sync::Arc;

use ecksport_rpc::client::RpcClient;
use ecksport_rpc::prelude::*;
use thiserror::Error;

use aspect_codec::ecksport::AsAspCodec;
use aspect_core::message_queue::MessageQueueId;
use aspect_ident::{DeviceIdent, DirPackage};

// FIXME make these consistent
use crate::errors::*;
use crate::identity;
use crate::msg_queue;
use crate::msg_queue::*;
use crate::user::*;

#[derive(Debug, Error)]
pub enum AspClientError {
    #[error("server responded with too many response entries")]
    TooManyResponses,

    #[error("announcement rejected: {0}")]
    AnnouncementRejected(String),

    #[error("channel closed without response")]
    NoResponse,

    #[error("protocol: {0}")]
    Asp(#[from] AspProtoError),

    #[error("ecks client: {0}")]
    EcksClient(#[from] ecksport_rpc::errors::ClientError),

    #[error("io: {0}")]
    Io(#[from] io::Error),

    #[error("codec: {0}")]
    Codec(#[from] aspect_codec::CodecError),

    #[error("not yet implemented")]
    Unimplemented,

    #[error("{0}")]
    Other(String),
}

/// High-level client for interacting with a homeserver and performing common
/// actions.
pub struct HomeserverClient {
    pub(crate) client: Arc<RpcClient>,
}

impl HomeserverClient {
    pub fn from_client(client: RpcClient) -> Self {
        Self {
            client: Arc::new(client),
        }
    }

    /// Requests a specific device ID.
    pub async fn get_device_id(
        &self,
        id: DeviceIdent,
    ) -> Result<Option<DirPackage>, AspClientError> {
        let req = identity::QueryRequest::Id(id);
        let res = self.ident_query(AsAspCodec::from(req)).await?;
        let mut resp = res.into_inner();
        if resp.devices.is_empty() {
            Ok(None)
        } else {
            Ok(Some(resp.devices.remove(0)))
        }
    }

    /// Requests all of the devices associated with a username.
    pub async fn get_user_devices(
        &self,
        username: String,
    ) -> Result<Vec<DirPackage>, AspClientError> {
        let req = identity::QueryRequest::User(username);
        let res = self.ident_query(AsAspCodec::from(req)).await?;
        Ok(res.into_inner().devices)
    }

    /// Announces a DIR package to the homeserver.
    pub async fn announce_devident_package(&self, pkg: DirPackage) -> Result<(), AspClientError> {
        let res = self.ident_announce(AsAspCodec::new(pkg)).await?;
        match res.into_inner() {
            identity::AnnResponse::Accepted(_) => Ok(()),
            identity::AnnResponse::Rejected(m) => Err(AspClientError::AnnouncementRejected(m)),
        }
    }

    /// Registers at the homesever with a username and password.
    pub async fn register_user(
        &self,
        username: String,
        password: String,
    ) -> Result<(), AspClientError> {
        self.user_register(username, password, Vec::new()).await?;
        Ok(())
    }

    /// Registers a device with the homeserver for an existing user, using their
    /// password.  The homeserver must have never seen this device before in
    /// order to accept the association.
    pub async fn register_device(
        &self,
        username: String,
        password: String,
        package: DirPackage,
    ) -> Result<(), AspClientError> {
        let creds = UserCreds { username, password };
        self.user_add_device(AsAspCodec::new(creds), AsAspCodec::new(package))
            .await?;
        Ok(())
    }

    /// Returns a list of the device message queues available to the calling device ID.
    pub async fn list_mqs(
        &self,
        username: String,
        password: String,
        device_ident: DeviceIdent,
    ) -> Result<Vec<QueueViewState>, AspClientError> {
        let creds = UserCreds { username, password };
        let req = MqListRequest {
            creds,
            device_ident,
        };
        let res = self.mq_list(AsAspCodec::new(req)).await?;
        Ok(res.into_inner().queues)
    }

    /// Returns the messages in a particular queue.
    pub async fn query_queue_messages(
        &self,
        username: String,
        password: String,
        device_id: DeviceIdent,
        queue_id: MessageQueueId,
        start_idx: u64,
        end_idx: u64,
    ) -> Result<MqEntries, AspClientError> {
        let creds = UserCreds { username, password };
        let req = MqFetchRange {
            creds,
            device_id,
            queue_id,
            start_idx,
            end_idx,
        };
        let res = self.mq_fetch(AsAspCodec::new(req)).await?;
        Ok(res.into_inner())
    }

    /// Sends a d2d message.
    ///
    /// XXX This proto isn't designed to be idempotent so if this fails it might
    /// have secretly still been processed and relayed.
    pub async fn send_d2d_msg(
        &self,
        username: String,
        password: String,
        device_id: DeviceIdent,
        target_device_id: DeviceIdent,
        payload: Vec<u8>,
    ) -> Result<(), AspClientError> {
        let creds = UserCreds { username, password };
        let req = MqDirectDeviceSendRequest {
            creds,
            device_id,
            target_device_id,
            payload,
        };

        self.mq_send_d2d(AsAspCodec::new(req)).await?;

        Ok(())
    }
}

ecksport_rpc::define_interface! {
    user(UserServ, HomeserverClient(client)) {
        "u.reg" user_register(username: String, password: String, cred: Vec<u8>) => ();
        "u.adddev" user_add_device(creds: AsAspCodec<UserCreds>, package: AsAspCodec<DirPackage>) => ();
    }
}

ecksport_rpc::define_interface! {
    identity(IdentServ, HomeserverClient(client)) {
        "i.query" ident_query(query: AsAspCodec<identity::QueryRequest>) => AsAspCodec<identity::QueryResponse>;
        "i.ann" ident_announce(ident: AsAspCodec<DirPackage>) => AsAspCodec<identity::AnnResponse>;
    }
}

ecksport_rpc::define_interface! {
    msgqueue(MqServ, HomeserverClient(client)) {
        "mq.ls" mq_list(req: AsAspCodec<msg_queue::MqListRequest>) => AsAspCodec<msg_queue::MqListResponse>;
        "mq.fetch" mq_fetch(range: AsAspCodec<msg_queue::MqFetchRange>) => AsAspCodec<msg_queue::MqEntries>;
        "mq.cs" mq_consume(req: AsAspCodec<msg_queue::ConsumeRequest>) => ();
        "mqs.d2d" mq_send_d2d(req: AsAspCodec<msg_queue::MqDirectDeviceSendRequest>) => ();
    }
}
